/*
 * StitchLinksAtNodeAction.java
 * 
 * Created by demory on Jan 4, 2009, 6:20:17 PM
 * 
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.fpdev.apps.admin.actions;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.fpdev.core.basenet.BLink;
import org.fpdev.core.basenet.BLinkStreet;
import org.fpdev.core.basenet.BNode;
import org.fpdev.core.basenet.Path;
import org.fpdev.core.transit.SubRoute;
import org.fpdev.core.transit.TransitPath;
import org.fpdev.apps.admin.AdminClient;

/**
 *
 * @author demory
 */
public class StitchLinksAtNodeAction implements ACAction {

  private BNode node_;

  private Set<DeleteLinkAction> deleteLinkActions_;
  private Set<CreateLinkAction> createLinkActions_;
  private Set<EditAddressRangesAction> editAddrRangeActions_;

  private DeleteNodeAction deleteNodeAction_;

  private Map<SubRoute, TransitPath> oldTPaths_;

  public StitchLinksAtNodeAction(BNode node) {
    node_ = node;
  }

  public boolean doAction(AdminClient av) {
    if(av.getEngine().getBaseNet().linkCount(node_) % 2 != 0) return false;

    // make copy of old transit paths
    if(oldTPaths_ == null) {
      oldTPaths_ = new HashMap<SubRoute, TransitPath>();
      for(BLink link : av.getEngine().getBaseNet().incidentLinks(node_)) {
        for(Path path : link.getRegisteredPaths()) {
          if (path.isTransitPath()) {
            TransitPath tPath = (TransitPath) path;
            SubRoute sub = tPath.getSubRoute();
            if(!oldTPaths_.containsKey(sub)) {
              TransitPath copy = tPath.createCopy(av.getEngine(), sub, false);
              oldTPaths_.put(sub, copy);
            }
          }
        }
      }
    }
    
    // check for stops at affected node
    boolean stopFound = false;
    for (SubRoute sub : oldTPaths_.keySet()) {
      if(sub.getPath().getStopCountAtNode(node_) > 0) {
        av.msg("Could not stitch links node "+node_.getID()+": "+sub.getMasterID()+" has stop");
        stopFound = true;
      }
    }
    if(stopFound) return false;

    // sort links by angle
    List<SortableLink> links = new LinkedList<SortableLink>();
    for(BLink link : av.getEngine().getBaseNet().getGraph().edgesOf(node_)) {
      Point2D.Double shpPt = link.getNearestShapePoint(node_);
      double dx = shpPt.x - node_.getX(), dy = shpPt.y - node_.getY();
      double len = Math.sqrt(dx*dx + dy*dy);
      double theta  = Math.acos(Math.abs(dx)/len); // angle from the x-axis
      if(dx <= 0 && dy > 0) // Quadrant II adjustment
        theta = Math.PI - theta;
      if(dx < 0 && dy <= 0) // Quadrant III adjustment
        theta = Math.PI + theta;
      if(dx >= 0 && dy < 0) // Quadrant IV adjustment
        theta = 2*Math.PI - theta;
      links.add(new SortableLink(link, theta));
    }
    Collections.sort(links);

    // check link compatibility
    for(int i=0; i<links.size()/2; i++) {
      BLink link1 = links.get(i).getLink();
      BLink link2 = links.get(i + links.size()/2).getLink();
      if(link1.getType() != link2.getType()) {
        av.msg("Could not stitch links "+link1.getID()+" & "+link2.getID()+" (different types)");
        return false;
      }
      if(link1.getScenario() != link2.getScenario()) {
        av.msg("Could not stitch links "+link1.getID()+" & "+link2.getID()+" (different scenarios)");
        return false;
      }
    }

    // join the links
    createLinkActions_ = new HashSet<CreateLinkAction>();
    editAddrRangeActions_ = new HashSet<EditAddressRangesAction>();
    for(int i=0; i<links.size()/2; i++) {
      BLink link1 = links.get(i).getLink();
      BLink link2 = links.get(i + links.size()/2).getLink();
      CreateLinkAction action = joinLinks(av, link1, link2);
      createLinkActions_.add(action);

      // re-register path(s) w/ new links
      BLink newLink = action.getLink();
      for(Path path : link1.getRegisteredPaths())
        newLink.registerPath(path);
      for(Path path : link2.getRegisteredPaths())
        newLink.registerPath(path);

      // update address range if applicable
      if(link1.getClassType() == BLink.CLASS_STREET && link2.getClassType() == BLink.CLASS_STREET) {
        BLinkStreet l1st = (BLinkStreet) link1, l2st = (BLinkStreet) link2;
        if(l1st.getDisplayName().equals(l2st.getDisplayName())) {
          int newFAddrL = 0, newTAddrL = 0, newFAddrR = 0, newTAddrR = 0;
          int newZipL = l1st.getLengthFeet() > l2st.getLengthFeet() ? l1st.getZipL() : l2st.getZipL();
          int newZipR = l1st.getLengthFeet() > l2st.getLengthFeet() ? l1st.getZipR() : l2st.getZipR();
          
          int l1stMidR = (l1st.getFAddrR() + l1st.getTAddrR()) / 2;
          int l1stMidL = (l1st.getFAddrL() + l1st.getTAddrL()) / 2;
          int l2stMidR = (l2st.getFAddrR() + l2st.getTAddrR()) / 2;
          int l2stMidL = (l2st.getFAddrL() + l2st.getTAddrL()) / 2;
          
          if(l1stMidL < l2stMidL) {
            newFAddrL = Math.min(l1st.getFAddrL(), l1st.getTAddrL());
            newTAddrL = Math.max(l2st.getFAddrL(), l2st.getTAddrL());
          }
          if(l1stMidL > l2stMidL) {
            newFAddrL = Math.max(l1st.getFAddrL(), l1st.getTAddrL());
            newTAddrL = Math.min(l2st.getFAddrL(), l2st.getTAddrL());
          }
          if(l1stMidR < l2stMidR) {
            newFAddrR = Math.min(l1st.getFAddrR(), l1st.getTAddrR());
            newTAddrR = Math.max(l2st.getFAddrR(), l2st.getTAddrR());
          }
          if(l1stMidR > l2stMidR) {
            newFAddrR = Math.max(l1st.getFAddrR(), l1st.getTAddrR());
            newTAddrR = Math.min(l2st.getFAddrR(), l2st.getTAddrR());
          }
          
          EditAddressRangesAction addrAction = new EditAddressRangesAction((BLinkStreet) newLink, newFAddrL, newFAddrR, newTAddrL, newTAddrR, newZipL, newZipR);
          addrAction.doAction(av);
          editAddrRangeActions_.add(addrAction);
        }
      }

      // update affected paths
      for (SubRoute sub : oldTPaths_.keySet()) {
        sub.getPath().stitchLinks(link1, link2, action.getLink());
      }

    }

    // delete old node and links
    deleteLinkActions_ = new HashSet<DeleteLinkAction>();
    Set<BLink> linksCopy = new HashSet<BLink>(av.getEngine().getBaseNet().getGraph().edgesOf(node_));
    for(BLink link : linksCopy )
      deleteLinkActions_.add(av.getNetworkOps().deleteLink(link, true));

    // delete new node
    deleteNodeAction_ = av.getNetworkOps().deleteNode(node_, true);

    if(!oldTPaths_.isEmpty()) av.getEngine().getRoutes().changesMade();

    return true;
  }

  
  public boolean undoAction(AdminClient av) {

    // restore the original transit paths
    for (Map.Entry<SubRoute, TransitPath> entry : oldTPaths_.entrySet()) {
      TransitPath path = entry.getValue().createCopy(av.getEngine(), false);
      entry.getKey().setPath(path);
      av.getGUI().getMapPanel().getDrawItems().transitPathItemUpdated(entry.getKey().getPath());
    }

    // undo the link/node modifications
    deleteNodeAction_.undoAction(av);
    for(DeleteLinkAction action : deleteLinkActions_) action.undoAction(av);
    for(CreateLinkAction action : createLinkActions_) action.undoAction(av);
    for(EditAddressRangesAction action : editAddrRangeActions_) action.undoAction(av);
    return true;
  }

  public String getName() {
    return "Stitch Connected Links";
  }

  //** joinLinks() and supporting methods **//

  private CreateLinkAction joinLinks(AdminClient av, BLink link1, BLink link2) {
    System.out.println("joining links: "+ link1.getID()+" & "+link2.getID());
    double newLen = link1.getLengthFeet()+link2.getLengthFeet();
    return av.getNetworkOps().newLink(0, link1.getDisplayName(), link1.getScenario(),
            getJoinedFNode(link1, link2), getJoinedTNode(link1, link2),
            link1.getType(), getJoinedShapePoints(link1, link2), newLen, true);
  }

  private BNode getJoinedFNode(BLink link1, BLink link2) {
    if(link1.getFNode() == link2.getFNode() || link1.getFNode() == link2.getTNode()) return link1.getTNode();
    if(link1.getTNode() == link2.getFNode() || link1.getTNode() == link2.getTNode()) return link1.getFNode();
    return null; // TODO: throw exception instead
  }

  private BNode getJoinedTNode(BLink link1, BLink link2) {
    if(link2.getFNode() == link1.getFNode() || link2.getFNode() == link1.getTNode()) return link2.getTNode();
    if(link2.getTNode() == link1.getFNode() || link2.getTNode() == link1.getTNode()) return link2.getFNode();
    return null; // TODO: throw exception instead
  }

  private BNode getJoinedCommonNode(BLink link1, BLink link2) {
    if(link1.getFNode() == link2.getFNode() || link1.getFNode() == link2.getTNode()) return link1.getFNode();
    if(link1.getTNode() == link2.getFNode() || link1.getTNode() == link2.getTNode()) return link1.getTNode();
    return null; // TODO: throw exception instead
  }

  private List<Point2D.Double> getJoinedShapePoints(BLink link1, BLink link2) {

    BNode commonNode = getJoinedCommonNode(link1, link2);

    List<Point2D.Double> shpPts = new ArrayList<Point2D.Double>();

    // add link1's shapepoints, reversing if necessary
    if(link1.shapePointsCount() > 0) {
      List<Point2D.Double>  link1ShpPts_ = new ArrayList<Point2D.Double>();
      link1ShpPts_.addAll(link1.getShapePointsList());
      if(link1.getFNode() == commonNode) Collections.reverse(link1ShpPts_);
      shpPts.addAll(link1ShpPts_);
    }

    // add the common node as a shapepoint
    shpPts.add(commonNode.toPoint());

    // add link2's shapepoints, reversing if necessary
    if(link2.shapePointsCount() > 0) {
      List<Point2D.Double>  link2ShpPts_ = new ArrayList<Point2D.Double>();
      link2ShpPts_.addAll(link2.getShapePointsList());
      if(link2.getTNode() == commonNode) Collections.reverse(link2ShpPts_);
      shpPts.addAll(link2ShpPts_);
    }

    return shpPts;
  }

  // HELPER CLASS

  private class SortableLink implements Comparable {

    private BLink link_;
    private double angle_;

    public SortableLink(BLink link, double angle) {
      link_ = link;
      angle_ = angle;
    }

    public BLink getLink() { return link_; }

    public int compareTo(Object o) {
      return new Double(angle_).compareTo(new Double(((SortableLink) o).angle_));
    }

  }

}
